var nodePath = require("path");
var defaultResolveFrom = require("resolve-from");
var env = process.env.NODE_ENV;
var production = !env || env !== "development";

function getRootDeps(template, context) {
  if (production && template.___depsArray) {
    return template.___depsArray;
  }

  attachDepsAndComponentsToTemplate(template, context);

  var deps = (template.___depsArray = Object.keys(template.___deps).map(
    (key) => template.___deps[key],
  ));
  var initModule = getInitModule(template.path, template.___components);

  if (initModule) deps.push(initModule);

  // these dependencies should be last
  deps.concat = function () {
    var result = [];
    result = result.concat.apply(result, arguments);
    result.push.apply(result, this);
    return result;
  };

  return deps;
}

function attachDepsAndComponentsToTemplate(target, context) {
  var template;

  if (!target.meta && target.template) {
    template = target.template;
  } else {
    template = target;
  }

  if (typeof template.createOut !== "function") return;
  if (production && target.___deps) return;

  var deps = (target.___deps = {});
  var components = (target.___components = {});

  if (!template.meta) {
    // eslint-disable-next-line no-console
    console.warn("Metadata not set for template at ", template.path);
    return;
  }

  var meta = template.meta;
  var root = nodePath.dirname(template.path);

  if (meta.deps) {
    meta.deps.forEach((dep) => {
      dep = resolveDep(dep, root, context);
      deps[dep.virtualPath || dep.path] = dep;
    });
  }

  if (meta.id && meta.component) {
    var resolveFrom = (context && context.resolveFrom) || defaultResolveFrom;
    components[meta.id] = {
      id: meta.id,
      path: resolveFrom(root, meta.component),
    };
  }

  if (meta.tags) {
    meta.tags.forEach((tagPath) => {
      var resolveFrom = context.resolveFrom || defaultResolveFrom;
      var tag = resolveFrom(root, tagPath);
      var ext = nodePath.extname(tag);
      var req = context.require || require;

      try {
        tag = req.resolve(tag.slice(0, 0 - ext.length) + ".js");
      } catch (e) {
        /* ignore error */
      }

      tag = req(tag);

      attachDepsAndComponentsToTemplate(tag, context);

      if (tag.___deps) Object.assign(deps, tag.___deps);
      if (tag.___components) Object.assign(components, tag.___components);
    });
  }
}

function getInitModule(path, components) {
  var module = null;

  if (components) {
    components = Object.keys(components).map((key) => components[key]);

    if (components.length) {
      var root = nodePath.dirname(path);
      var virtualPath = path + ".init.js";
      var registrations = components.map(
        (component) =>
          `components.register('${component.id}', require('.${
            nodePath.sep
          }${nodePath.relative(root, component.path)}'));`,
      );
      var code = `
                var components = require('marko/components');
                ${registrations.join("\n")}
            `;

      module = {
        type: "require",
        run: true,
        virtualModule: {
          path: virtualPath,
          read: function (_, callback) {
            // TODO: Check if read can just return string in lasso 2
            if (callback) {
              callback(null, code);
            } else {
              return code;
            }
          },
        },
      };
    }
  }

  return module;
}

function resolveDep(dep, root, context) {
  if (typeof dep === "string") {
    dep = parseDependencyString(dep);
  }

  if (dep.path) {
    var resolveFrom = (context && context.resolveFrom) || defaultResolveFrom;
    dep.path = resolveFrom(root, dep.path);

    if (dep.path && !dep.type) {
      dep.type = dep.path.slice(dep.path.lastIndexOf(".") + 1);
    }
  }

  if (dep.virtualPath) {
    dep.virtualPath = nodePath.resolve(root, dep.virtualPath);
  }

  if (dep.type === "js") {
    dep.type = "require";
    dep.run = true;
  }

  return dep;
}

function parseDependencyString(string) {
  var match = /^(?:([\w-]+)(?::\s*|\s+))?(.*?(?:\.(\w+))?)$/.exec(string);
  return {
    type: match[1] || match[3],
    path: match[2],
  };
}

function patch(Template) {
  Template.prototype.getDependencies = function (context) {
    context = context || {};

    return getRootDeps(this, context);
  };
}

exports.getDeps = getRootDeps;
exports.resolveDep = resolveDep;
exports.patch = patch;
